#!/usr/bin/env python

# Possum's Image Viewer
#
# Released under the GPLv3 at http://www.gnu.org/copyleft/gpl.html
# Designed for pygtk and the Maemo platform.
#
# Copyright 2008 by Jackson Yee (jackson@gotpossum.com)

import pygtk
pygtk.require('2.0')

import gtk
import gtk.gdk as gdk
import gtk.keysyms as keysyms
import sys
import os
import os.path
import gobject
import random
import gc

from lumenn.config import Config
from lumenn.image import Cache, Image, Thumbnail
from lumenn.files import FileList, FileInfo
import lumenn.log as log
from lumenn.db import DB, ListAdapter

from optparse import OptionParser
import cStringIO as StringIO

#	Global script variables
AppName 		= "Possum's Image Viewer"
AppVersion	= '1.20081201'
AppDir			=	'possum-image-viewer'
conf				= Config(AppDir)

db					=	DB( conf.GetFileName('filedb') )
fileregexp	= '.*\.(jpg|jpeg|gif|png|tga|tif|tiff|bmp|svg)$'

# *********************************************************************
class MainWnd(gtk.Window):
	
	# -------------------------------------------------------------------
	def __init__(self):
		gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
		
		imgcachesize = conf.get('imgcachesize', 2)
		thumbcachesize = conf.get('thumbcachesize', 4)
		thumbsize = conf.get('thumbsize', (100, 100) )
		
		self.image = Image( Cache(imgcachesize) )
		
		self.thumbnail = Thumbnail( self.image, 
			Cache( thumbcachesize, thumbsize ) 
		)
		
		self.thumbnail.filelist	=	ListAdapter(db, FileInfo)
		
		if len(sys.argv) > 2:
			self.expression	=	sys.argv[2]
		else:
			self.expression	=	None
		
		if len(sys.argv) > 1:
			if self.expression:
				self.thumbnail.filelist.SetFilter(
					"WHERE dirname LIKE '%s%%' AND filename LIKE'%%%s%%'" % (
						os.path.abspath(sys.argv[1]),
						self.expression,
					),
				)
			else:
				self.thumbnail.filelist.SetFilter(
					"WHERE dirname LIKE '%s%%'" % os.path.abspath(sys.argv[1])
				)
			self.dirname	=	sys.argv[1]
		else:
			self.dirname = None				
		
		self.image.postimageloaded = self.UpdateText
		self.image.fittowindow	=	conf.get('fittowindow', True)
		sort = conf.get('sort', ['dirname', 'filename'] )
		self.thumbnail.filelist.sort = sort		
		
		self.table = gtk.Table(2, 2)
		self.table.attach(self.image, 0, 1, 0, 2)
		self.table.attach(self.thumbnail, 1, 2, 0, 1, xoptions = 0)
		
		self.add(self.table)
		
		thumbwidth = conf.get('thumbwidth', 1) * (thumbsize[0] + (self.thumbnail.border * 2) )		
		
		self.thumbnail.set_size_request(thumbwidth, -1)
		
		x, y, w, h = conf.get('wndpos', (0, 0, 640, 480) )
		
		if w < 400:
			w = 400
		
		if h < 300:
			h = 300
			
		self.move(x, y)
		self.resize(w, h)
		
		b = conf.get('fullscreen', False)
		conf['fullscreen'] = b
		
		if b:
			self.fullscreen()
		
		self.preload = conf.get('preload', False)
		
		self.set_title(AppName + ' - ' + AppVersion)
		
		gobject.idle_add( self.Load )
		
		self.connect('destroy', self.OnDestroy)
		self.connect('delete_event', self.OnDelete)
		self.connect('key_press_event', self.OnKeyPress)
		self.thumbnail.connect('selected', self.OnThumbSelect)
		self.connect('filesearch_update', self.OnFileSearchUpdate)
		self.connect('filesearch_finished', self.OnFileSearchFinished)
		self.connect("configure_event", self.OnConfigure)
		
		self.show_all()
		
		self.set_size_request(100, 100)
		
		self.menumode	=	'menubutton'
					
	# -------------------------------------------------------------------
	def OnConfigure(self, wnd, evt, data = None):
		if not self.image.get_property('visible'):
			self.lcars.set_size_request(evt.width, evt.height)
		
	# -------------------------------------------------------------------
	def OnKeyPress(self, wnd, evt, data = None):
		if evt.keyval == keysyms.space or evt.keyval == keysyms.Right:
			self.Next()
		elif evt.keyval == keysyms.BackSpace or evt.keyval == keysyms.Left:
			self.Previous()
		elif evt.keyval == keysyms.r or evt.keyval == keysyms.Up:
			self.Random()
		elif evt.keyval == keysyms.g:
			self.Goto()
		elif evt.keyval == keysyms.F7:
			self.ZoomIn()
		elif evt.keyval == keysyms.F8:
			self.ZoomOut()
		elif evt.keyval == keysyms.F4:
			if self.menumode == 'mainmenu':
				self.OnToggleMenu('menubutton')
			else:
				self.OnToggleMenu('mainmenu')
		elif evt.keyval == keysyms.q or evt.keyval == keysyms.Escape:
			self.Quit()
		elif evt.keyval == keysyms.f or evt.keyval == keysyms.F6:
			self.FullScreen()
		elif evt.keyval == keysyms.d or evt.keyval == keysyms.Down:
			self.DeleteFile()
		elif evt.keyval == keysyms.o or evt.keyval == keysyms.Return:
			self.ChooseDirectory()		
		elif evt.keyval == keysyms.l:
			self.image.Rotate(90)		
		elif evt.keyval == keysyms.semicolon:
			self.image.Rotate(-90)		
		elif evt.keyval == keysyms.s:
			self.Save()
		elif evt.keyval == keysyms.i:
			self.image.OriginalSize()
			self.UpdateText()
		elif evt.keyval == keysyms.t:
			self.image.DrawStatus('Reloading current directory...')
			self.LoadDir(self.dirname)
		
	# -------------------------------------------------------------------
	def OnDelete(self, wnd, data = None):
		if not conf['fullscreen']:
			x, y = self.get_position()
			w, h = self.get_size()
			conf['wndpos'] = (x, y, w, h)
			
		conf['thumbwidth'] = int(self.thumbnail.get_allocation().width / self.thumbnail.cache.scaling[0])
		conf['imgcachesize'] = self.image.cache.size
		conf['thumbcachesize'] = self.thumbnail.cache.size
		conf['thumbsize'] = self.thumbnail.cache.scaling
		conf['sort'] = self.thumbnail.filelist.sort
		conf['preload'] = self.preload
		
	# -------------------------------------------------------------------
	def OnDestroy(self, wnd, data = None):
		gtk.main_quit()
		
	# -------------------------------------------------------------------
	def OnThumbSelect(self, wnd, pos):
		self.Load()
		
	# -------------------------------------------------------------------
	def OnPaneMove(self, wnd, evt):
		p = self.hpaned.get_position() 
		ts = self.thumbnail.cache.scaling[0] + self.thumbnail.border
		
		np = int( ( (p + ts / 2 ) / ts) * ts ) + 1
		
		if np < ts:
			np = ts
		
		if np != p:
			self.hpaned.set_position(np)
			
		self.UpdateText()
		
	# -------------------------------------------------------------------
	def OnFileSearchUpdate(self, widget, numfiles):
		self.image.DrawStatus('Searching... found %d files so far' % numfiles)
		
	# -------------------------------------------------------------------
	def OnFileSearchFinished(self, widget, dirname, exception):			
		if exception:
			log.Log(log.ERROR, 'Problem loading %s: %s' % (dirname, exception) )
			self.image.DrawStatus('Problem loading %s: %s' % (dirname, exception) )
			return			
		
		if len(self.thumbnail):
			self.Load()
			self.thumbnail.queue_draw()
			
	# -------------------------------------------------------------------
	def OnToggleMenu(self, args):			
		rt = self.get_allocation()
		
		m = args
		
		if m == 'menubutton':
			self.thumbnail.show()
			self.lcars.set_size_request(*self.lcarssize)
			self.lcars.Load( self.lcarsmenus[m] )
		elif m == 'mainmenu':
			self.thumbnail.hide()
			self.lcars.set_size_request(self.lcarssize[0], rt.height)
			self.lcars.Load( self.lcarsmenus[m] )
		
		self.menumode	=	m
		
	# -------------------------------------------------------------------
	def UpdateText(self, image):
		if self.image.filename:
			w, h	=	self.image.GetDimensions()
			self.image.statustext = '%d/%d: %s (%d%% from %dx%d) in %s' % (
				self.thumbnail.GetPos() + 1, 
				len(self.thumbnail), 
				os.path.basename(self.image.filename),
				int(self.image.magnification * 100),
				w,
				h,
				os.path.basename( os.path.dirname(self.image.filename) ),
			)
		
	# -------------------------------------------------------------------
	def Run(self):
		gtk.main()
		
	# -------------------------------------------------------------------
	def LoadDir(self, dirname):
		dirname	=	os.path.abspath(dirname)
		try:
			ScanDir(dirname)
			
			gobject.idle_add(self.emit, 
				'filesearch_finished', 
				None, 
				None)			
			
			self.thumbnail.filelist.SetFilter("WHERE dirname LIKE '%s%%'" % dirname)
			self.thumbnail.SetPos(0)
			self.Load()
		except Exception, e:
			log.Log(log.ERROR, 'Unable to search directory %s: %s' % (dirname, e) )
			gobject.idle_add(self.emit, 'filesearch_finished', dirname, e)
		
	# -------------------------------------------------------------------
	def Load(self):
		t = self.thumbnail
		pos = self.thumbnail.GetPos()
		s = len(t) 
		
		if s == 0:
			return
		
		self.image.DrawStatus('Loading...')
		self.image.Load( t[pos].GetPath() )
		
		if self.preload:
			gobject.idle_add(self.Preload)
	
	# -------------------------------------------------------------------
	def Preload(self):
		if not len(self.thumbnail.filelist):
			return
			
		t = self.thumbnail
		pos = self.thumbnail.GetPos()
		s = len(t) 
		
		if pos < s - 1:
			self.image.cache.Load( t[ pos + 1 ].GetPath())
			
	# -------------------------------------------------------------------
	def Next(self):
		if not len(self.thumbnail.filelist):
			return
		
		t = self.thumbnail
		pos = self.thumbnail.GetPos()
		s = len(t) 
		
		if pos < s - 1:
			t.SetPos( pos + 1 )
		else:
			t.SetPos( 0 )
		
		self.Load()
		
	# -------------------------------------------------------------------
	def Previous(self):
		if not len(self.thumbnail.filelist):
			return
			
		t = self.thumbnail
		pos = self.thumbnail.GetPos()
		s = len(t) 
		
		if pos > 0:
			t.SetPos( pos - 1 )
		else:
			t.SetPos( s - 1 )
		
		self.Load()		
		
	# -------------------------------------------------------------------
	def Random(self):
		if not len(self.thumbnail.filelist):
			return
			
		t = self.thumbnail
		s = len(t) 
		t.SetPos( random.randint(0, s - 1) )			
		self.Load()
		
	# -------------------------------------------------------------------
	def Goto(self):
		d = gtk.Dialog('Goto...',
			self,
			gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
			(	'Goto', gtk.RESPONSE_OK, 
				'Cancel', gtk.RESPONSE_CANCEL,
			),
		)
		
		max = len(self.thumbnail.filelist)
		
		d.entry = gtk.Entry( len( str( max ) ) )
		d.entry.connect("activate", lambda e, d, r: d.response(r), d, gtk.RESPONSE_OK)
		
		d.vbox.set_spacing(8)
		d.vbox.add( gtk.Label('Type the number of the picture to load') )
		d.vbox.add(d.entry)
		d.vbox.add( gtk.Label('out of %i' % max) )
		d.show_all()
		
		d.set_default_response(gtk.RESPONSE_OK)
		
		if d.run() == gtk.RESPONSE_OK:
			pos = int( d.entry.get_text() ) - 1
			
			if pos < 0:
				pos = 0
				
			if pos > max - 1:
				pos = max - 1
				
			self.thumbnail.SetPos(pos)
			self.Load()
		
		d.destroy()
		
	# -------------------------------------------------------------------
	def FullScreen(self):
		b = conf.get('fullscreen', False)
		
		if not b:
			self.fullscreen()
		else:
			self.unfullscreen()
		
		conf['fullscreen'] = not b
		gobject.idle_add(self.Load)
		
	# -------------------------------------------------------------------
	def DeleteFile(self):
		t = self.thumbnail
		pos = self.thumbnail.GetPos()
		s = len(t)
		f	=	t[pos]
		
		if not s:
			return
		
		d = gtk.MessageDialog(type = gtk.MESSAGE_QUESTION,
					buttons = gtk.BUTTONS_YES_NO,
					message_format = """Are you sure that you want to delete %s?
					
Once this file is deleted, 
you will lose it forever
unless you have a backup of it somewhere."""
						% f.filename,
					)
		
		if d.run() == gtk.RESPONSE_YES:
			os.remove( f.GetPath() )
			
			try:
				os.remove( os.path.join( f.dirname, '.thumbnails', f.filename ) )
			except:
				pass
			
			del t[pos]
			
			if len(t) > pos:
				t.SetPos( pos )
			else:
				t.SetPos( pos - 1 )
				
			self.Load()
		
		d.destroy()
			
	# -------------------------------------------------------------------
	def ChooseDirectory(self):
		d = gtk.FileChooserDialog(title = 'Please choose a directory to browse',
					action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
					buttons = ('Load', gtk.RESPONSE_OK, 
											'Cancel', gtk.RESPONSE_CANCEL,
										)
				)
		
		if d.run() == gtk.RESPONSE_OK:
			self.dirname = d.get_filename()			
			self.lastpos = 0
			
			gobject.idle_add(self.LoadDir, self.dirname)
			
		d.destroy()	
		
	# -------------------------------------------------------------------
	def GetTags(self, filename):
		start = filename.find('[')
		end = filename.rfind(']')
		
		if start == -1 or end == -1 or end < start:
			return []
		
		return filename[start + 1:end].split(',')
		
	# -------------------------------------------------------------------
	def SetTags(self, filename, tags):
		start = filename.find('[')
		end = filename.rfind(']')
		ext = filename.rfind('.')
		
		if start == -1:
			pre = filename[0:ext]
		else:
			pre = filename[0:start]
		
		if end == -1:
			post = filename[ext:]
		else:
			post = filename[end + 1:]
			
		return '%s[%s]%s' % (pre, ','.join(tags), post)
		
	# -------------------------------------------------------------------
	def Quit(self):
		self.OnDelete(self, None)
		self.destroy()
		
	# -------------------------------------------------------------------
	def ZoomIn(self):
		self.image.DrawStatus('Zooming in...')
		self.image.Zoom(1.5)
		
	# -------------------------------------------------------------------
	def ZoomOut(self):
		self.image.DrawStatus('Zooming out...')
		self.image.Zoom( float(2) / 3)
		
	# -------------------------------------------------------------------
	def Save(self):
		if self.image.magnification != 1.0:
			self.image.OriginalSize()
			return
			
		d = gtk.FileChooserDialog(title = 'What would you like to call the new file?',
			action = gtk.FILE_CHOOSER_ACTION_SAVE,
			buttons = ('Save', gtk.RESPONSE_OK, 
				'Cancel', gtk.RESPONSE_CANCEL,
			),
		)
			
		d.set_filename(self.image.filename)
		d.set_do_overwrite_confirmation(True)
		
		if d.run() == gtk.RESPONSE_OK:
			f = d.get_filename()
			self.image.Save(f)
				
		d.destroy()

# *********************************************************************
gobject.signal_new("filesearch_update", 
	MainWnd,
	gobject.SIGNAL_ACTION,
	gobject.TYPE_NONE,
	(gobject.TYPE_PYOBJECT,), 
)

# *********************************************************************
gobject.signal_new("filesearch_finished", 
	MainWnd,
	gobject.SIGNAL_ACTION,
	gobject.TYPE_NONE,
	(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,),
)

# =====================================================================
def Run():
	gtk.gdk.threads_init()
	conf.Load()
	wnd = MainWnd()
	wnd.Run()
	conf.Save()

# =====================================================================
def SaveToDB(filelist, dirname):
	if len(filelist) > 2500:
		db.Save(filelist.ls)
		filelist.Clear()
		gc.collect()

# =====================================================================
def ScanDir(dirname):
	log.Log(log.ACTION, 'Scanning directory %s for image files...' % dirname)
	
	filelist	=	FileList()
	
	db.Delete( FileInfo, 
		"WHERE dirname LIKE '%s%%'" % dirname,
	)
	
	filelist.Load(dirname, 
		fileregexp, 
		'.*\.thumbnails.*', 
		True,
		SaveToDB,
	)
	
	db.Save(filelist.ls)

# =====================================================================
def MakeThumbnails():
	log.Log(log.ACTION, 'Making thumbnails for all files in database...')
	
	ls	=	ListAdapter(db, FileInfo)
	thumbsize	=	conf.get('thumbsize', (100, 100) )
	cache	=	Cache( 1, thumbsize, False )
	
	for f in ls:
		p	=	f.GetPath()
		try:
			if not cache.HasThumb(p):
				cache.Load(p)
				log.Log(log.ACTION, 'Made thumbnail for %s' % p)
		except Exception, e:
			log.Log(log.ERROR, 'Unable to make thumbnail for %s: %s' % (p, e) )
			db.Delete(f)

# =====================================================================
def SaveToBuf(data, buffer):
	buffer.write(data)

# =====================================================================
def ReCompress(dirname):
	log.Log(log.ACTION, 'Scanning %s for undercompressed files...' 
		% dirname
	)
				
	ls	=	ListAdapter(db, FileInfo)
	ls.filter	=	"WHERE dirname LIKE '%s%%'" % dirname
	
	for f in ls:
		p, ext	=	f.GetPath(), f.GetExtension()
		
		if ext.lower() != '.jpg':
			continue
		
		newfile	=	'%s%s' % (p, ext)		
		
		try:
			img = gdk.pixbuf_new_from_file(p)
			buf	=	StringIO.StringIO()
			
			gc.collect()
			
			img.save_to_callback( SaveToBuf,
				'jpeg',
				{
					'quality'	:	'90',
				},
				buf,
			)
			
			v	=	buf.getvalue()
			
			oldsize	=	f.size
			newsize	=	len(v)
			
			if newsize < oldsize * 0.7:
				os.remove(p)
				
				nf	=	open(p, 'wb')
				nf.write( v )
				nf.close()
				
				log.Log(log.ACTION, 'Recompressed %s from %0.3f MiB to %0.3f MiB' 
					% (p, 
						oldsize / 1024.0 / 1024, 
						newsize / 1024.0 / 1024,
					) 
				)		

				f.size	=	newsize
				db.Save(f)
			
		except Exception, e:
			log.Log(log.ERROR, 'Unable to recompress %s to %s: %s' % (p, newfile, e) )

# *********************************************************************
if __name__ == '__main__':
	parser = OptionParser()
	conf.Load()
	
	parser.add_option('-s', 
		'--scandir', 
		dest	=	'scandir',
		help	=	'Scans directory to update files database')
		
	parser.add_option('-t', 
		'--thumbnail', 
		dest		=	'thumbnail',
		action	=	"store_true",
		help		=	'Create thumbnails of all images in database')

	parser.add_option('-c', 
		'--compress', 
		dest	=	'compressdir',
		help	=	'Recompress all images in directory for optimal size')
		
	parser.add_option('-l', 
		'--log', 
		dest	=	'logfile',
		help	=	'Logs all activity to file')
		
	parser.add_option('-e', 
		'--expression', 
		dest	=	'expression',
		help	=	'Shows only files containing this expression')
		
	parser.add_option('-v', 
		'--loglevel', 
		dest	=	'loglevel',
		help	=	"""Sets output level of logging. Values can be: 
0 (None)
1	(Errors)
2 (Warnings [default])
3 (Actions)
4 (Trace)
9	(All)""")
		
	(options, args) = parser.parse_args()
	
	if options.logfile:
		log.SetLog( FileLog( options.logfile ) )

	if options.loglevel:
		log.SetLogLevel( int(options.loglevel) )
		
	if options.scandir:
		ScanDir( os.path.abspath(options.scandir) )
	elif options.thumbnail:
		MakeThumbnails()
	elif options.compressdir:
		ReCompress( os.path.abspath(options.compressdir) )
	else:
		Run()
	
	conf.Save()
